package configs

import (
	"fmt"
	"strings"
	"tmlake.com/api-gateway/istio-agent/xds/xdsclient/xdsresource"
)

const (
	HeaderMethod    = ":method"
	HeaderAuthority = ":authority"
	HeaderScheme    = ":scheme"

	MethodVariable         = "$request_method"
	AuthorityVariable      = "$host"
	SchemeVariable         = "$scheme"
	internalLocationPrefix = "internal_location_"

	UpstreamName404 = "UpstreamName404"
)

type variableNamer struct {
	safeName string
}

func newVariableNamer(virtualHost *xdsresource.VirtualHost) *variableNamer {
	safeName := strings.ReplaceAll(fmt.Sprintf("%s", virtualHost.Name), "-", "_")
	safeName = strings.ReplaceAll(safeName, ".", "_")
	safeName = strings.ReplaceAll(safeName, ":", "_")
	return &variableNamer{
		safeName: safeName,
	}
}

// Condition defines a condition in a MatchRule.
type Condition struct {
	Header   string `json:"header"`
	Cookie   string `json:"cookie"`
	Argument string `json:"argument"`
	Variable string `json:"variable"`
	Value    string `json:"value"`
}

// Action defines an action.
type Action struct {
	Pass string `json:"pass"`
}

// Split defines a split.
type Split struct {
	Weight uint32  `json:"weight"`
	Action *Action `json:"action"`
}

// Match defines a match.
type Match struct {
	//&&
	Conditions []Condition `json:"conditions"`
	Action     *Action     `json:"action"`
	Splits     []Split     `json:"splits"`
}

// Route defines a route.
type Route struct {
	Path     string    `json:"path"`
	PathType *PathType `json:"pathType"`
	Name     string    `json:"name"`
	Action   *Action   `json:"action"`
	Splits   []Split   `json:"splits"`
	Matches  []Match   `json:"matches"`
}

type routingCfg struct {
	Maps                     []Map
	SplitClients             []SplitClient
	Locations                []*Location
	InternalRedirectLocation InternalRedirectLocation
	//ReturnLocations          []ReturnLocation
}

func (route *Route) Equal(r *Route) bool {
	if route == r {
		return true
	}
	if route == nil || r == nil {
		return false
	}

	if !route.Action.Equal(r.Action) {
		return false
	}

	if len(route.Splits) != len(r.Splits) {
		return false
	} else if len(route.Splits) != 0 {
		for _, sp1 := range route.Splits {
			found := false
			for _, sp2 := range r.Splits {
				if sp1.Equal(&sp2) {
					found = true
					break
				}
			}
			if !found {
				return false
			}
		}
	}

	if len(route.Matches) != len(r.Matches) {
		return false
	} else if len(route.Matches) != 0 {
		for _, m1 := range route.Matches {
			found := false
			for _, m2 := range r.Matches {
				if m1.Equal(&m2) {
					found = true
					break
				}
			}
			if !found {
				return false
			}
		}
	}

	return true
}
func (a1 *Action) Equal(a2 *Action) bool {
	if a1 == a2 {
		return true
	}
	if a1 == nil || a2 == nil {
		return false
	}
	if a1.Pass == a2.Pass {
		return true
	} else {
		return false
	}
	return true
}

func (sp1 *Split) Equal(sp2 *Split) bool {
	if sp1 == sp2 {
		return true
	}
	if sp1 == nil && sp2 == nil {
		return false
	}
	if sp1.Action.Equal(sp2.Action) && sp1.Weight == sp2.Weight {
		return true
	} else {
		return false
	}
	return true
}

func (m1 *Match) Equal(m2 *Match) bool {
	if m1 == m2 {
		return true
	}
	if m1 == nil || m2 == nil {
		return false
	}

	if len(m1.Conditions) != len(m2.Conditions) {
		return false
	}
	if len(m1.Splits) != len(m2.Splits) {
		return false
	}
	if !m1.Action.Equal(m2.Action) {
		return false
	}
	if len(m1.Conditions) != 0 {
		for _, c1 := range m1.Conditions {
			found := false
			for _, c2 := range m2.Conditions {
				if c1.Header == c2.Header && c1.Variable == c2.Variable && c1.Argument == c2.Argument && c1.Cookie == c1.Cookie && c1.Value == c2.Value {
					found = true
					break
				}
			}

			if !found {
				return false
			}
		}
	}

	if len(m1.Splits) != 0 {
		for _, s1 := range m1.Splits {
			found := false
			for _, s2 := range m2.Splits {
				if s1.Equal(&s2) {
					found = true
					break
				}
			}

			if !found {
				return false
			}
		}
	}
	return true
}

func getNameForSourceForMatchesRouteMapFromCondition(condition Condition) string {
	if condition.Header != "" {
		return fmt.Sprintf("$http_%s", strings.ReplaceAll(condition.Header, "-", "_"))
	}

	if condition.Cookie != "" {
		return fmt.Sprintf("$cookie_%s", condition.Cookie)
	}

	if condition.Argument != "" {
		return fmt.Sprintf("$arg_%s", condition.Argument)
	}

	return condition.Variable
}

func (namer *variableNamer) GetNameForVariableForMatchesRouteMap(
	matchesIndex int,
	matchIndex int,
	conditionIndex int,
) string {
	return fmt.Sprintf("$vs_%s_matches_%d_match_%d_cond_%d", namer.safeName, matchesIndex, matchIndex, conditionIndex)
}

func (namer *variableNamer) GetNameForVariableForMatchesRouteMainMap(matchesIndex int) string {
	return fmt.Sprintf("$vs_%s_matches_%d", namer.safeName, matchesIndex)
}

func (namer *variableNamer) GetNameForSplitClientVariable(index int) string {
	return fmt.Sprintf("$vs_%s_splits_%d", namer.safeName, index)
}

func generateValueForMatchesRouteMap(matchedValue string) (value string, isNegative bool) {
	if len(matchedValue) == 0 {
		return `""`, false
	}

	if matchedValue[0] == '!' {
		isNegative = true
		matchedValue = matchedValue[1:]
	}

	return fmt.Sprintf(`"%s"`, matchedValue), isNegative
}

func generateParametersForMatchesRouteMap(matchedValue string, successfulResult string) []Parameter {
	value, isNegative := generateValueForMatchesRouteMap(matchedValue)

	valueResult := successfulResult
	defaultResult := "0"
	if isNegative {
		valueResult = "0"
		defaultResult = successfulResult
	}

	params := []Parameter{
		{
			Value:  value,
			Result: valueResult,
		},
		{
			Value:  "default",
			Result: defaultResult,
		},
	}

	return params
}

func generateLocationForProxying(path string, upstreamName string, internal bool) Location {
	return Location{
		Path:         path,
		Internal:     internal,
		UpstreamName: upstreamName,
	}
}

func generateLocation(path string, upstreamName string, internal bool) Location {
	return generateLocationForProxying(path, upstreamName, internal)
}

func generateSplits(
	splits []Split,
	variableNamer *variableNamer,
	scIndex int) (SplitClient, []*Location) {
	var distributions []Distribution

	for i, s := range splits {
		d := Distribution{
			Weight: fmt.Sprintf("%d%%", s.Weight),
			Value:  fmt.Sprintf("/%vsplits_%d_split_%d", internalLocationPrefix, scIndex, i),
		}
		distributions = append(distributions, d)
	}

	splitClient := SplitClient{
		Source:        "$request_id",
		Variable:      variableNamer.GetNameForSplitClientVariable(scIndex),
		Distributions: distributions,
	}

	var locations []*Location

	for i, s := range splits {
		path := fmt.Sprintf("/%vsplits_%d_split_%d", internalLocationPrefix, scIndex, i)
		upstreamName := s.Action.Pass
		loc := generateLocation(path, upstreamName, true)
		locations = append(locations, &loc)
	}

	return splitClient, locations
}

func generateDefaultSplitsConfig(
	route *Route,
	variableNamer *variableNamer,
	scIndex int,
) routingCfg {
	sc, locs := generateSplits(route.Splits, variableNamer, scIndex)

	splitClientVarName := variableNamer.GetNameForSplitClientVariable(scIndex)

	irl := InternalRedirectLocation{
		Path:        route.Path,
		Destination: splitClientVarName,
	}

	return routingCfg{
		SplitClients:             []SplitClient{sc},
		Locations:                locs,
		InternalRedirectLocation: irl,
	}
}

func generateMatchesConfig(route *Route, variableNamer *variableNamer, index int, scIndex int) routingCfg {
	// Generate maps
	var maps []Map

	for i, m := range route.Matches {
		for j, c := range m.Conditions {
			source := getNameForSourceForMatchesRouteMapFromCondition(c)
			variable := variableNamer.GetNameForVariableForMatchesRouteMap(index, i, j)
			successfulResult := "1"
			if j < len(m.Conditions)-1 {
				successfulResult = variableNamer.GetNameForVariableForMatchesRouteMap(index, i, j+1)
			}

			params := generateParametersForMatchesRouteMap(c.Value, successfulResult)

			matchMap := Map{
				Source:     source,
				Variable:   variable,
				Parameters: params,
			}
			maps = append(maps, matchMap)
		}
	}

	scLocalIndex := 0

	// Generate the main map
	source := ""
	var params []Parameter
	for i, m := range route.Matches {
		source += variableNamer.GetNameForVariableForMatchesRouteMap(index, i, 0)

		v := fmt.Sprintf("~^%s1", strings.Repeat("0", i))
		r := fmt.Sprintf("/%vmatches_%d_match_%d", internalLocationPrefix, index, i)
		if len(m.Splits) > 0 {
			r = variableNamer.GetNameForSplitClientVariable(scIndex + scLocalIndex)
			scLocalIndex++
		}

		p := Parameter{
			Value:  v,
			Result: r,
		}
		params = append(params, p)
	}

	defaultResult := fmt.Sprintf("/%vmatches_%d_default", internalLocationPrefix, index)
	if len(route.Splits) > 0 {
		defaultResult = variableNamer.GetNameForSplitClientVariable(scIndex + scLocalIndex)
	}

	defaultParam := Parameter{
		Value:  "default",
		Result: defaultResult,
	}
	params = append(params, defaultParam)

	variable := variableNamer.GetNameForVariableForMatchesRouteMainMap(index)

	mainMap := Map{
		Source:     source,
		Variable:   variable,
		Parameters: params,
	}
	maps = append(maps, mainMap)

	// Generate locations for each match and split client
	var locations []*Location
	var splitClients []SplitClient
	scLocalIndex = 0

	for i, m := range route.Matches {
		if len(m.Splits) > 0 {

			sc, locs := generateSplits(
				m.Splits,
				variableNamer,
				scIndex+scLocalIndex)
			scLocalIndex++
			splitClients = append(splitClients, sc)
			locations = append(locations, locs...)
		} else {
			path := fmt.Sprintf("/%vmatches_%d_match_%d", internalLocationPrefix, index, i)
			upstreamName := m.Action.Pass
			loc := generateLocation(path, upstreamName, true)
			locations = append(locations, &loc)
		}
	}

	// Generate default splits or default action
	if len(route.Splits) > 0 {
		sc, locs := generateSplits(
			route.Splits,
			variableNamer,
			scIndex+scLocalIndex,
		)
		splitClients = append(splitClients, sc)
		locations = append(locations, locs...)
	} else {
		path := fmt.Sprintf("/%vmatches_%d_default", internalLocationPrefix, index)
		var upstreamName string
		//The default route is not configured. Only the advanced route is configured
		if route.Action.Pass == "" {
			upstreamName = UpstreamName404
		} else {
			upstreamName = route.Action.Pass
		}

		loc := generateLocation(path, upstreamName, true)
		locations = append(locations, &loc)
	}

	// Generate an InternalRedirectLocation to the location defined by the main map variable
	irl := InternalRedirectLocation{
		Path:        route.Path,
		Destination: variable,
	}

	return routingCfg{
		Maps:                     maps,
		Locations:                locations,
		InternalRedirectLocation: irl,
		SplitClients:             splitClients,
	}
}

func iSVariable(headerName string) bool {
	switch headerName {
	case HeaderMethod, HeaderScheme, HeaderAuthority:
		return true
	}

	return false
}

func getVariable(headerName string) string {
	switch headerName {
	case HeaderMethod:
		return MethodVariable
	case HeaderScheme:
		return SchemeVariable
	case HeaderAuthority:
		return AuthorityVariable
	}

	return ""
}
func GenerateMatch(route *xdsresource.Route) Match {
	match := Match{
		Conditions: []Condition{},
		Splits:     []Split{},
		Action:     &Action{},
	}
	if route == nil || len(route.Headers) == 0 && len(route.QueryParams) == 0 {
		return match
	}

	for _, header := range route.Headers {
		invert := header.InvertMatch
		var value string
		switch header.MatchPattern {
		case xdsresource.ExactMatcherType:
			if *invert {
				value = "!" + *header.ExactMatch
			} else {
				value = *header.ExactMatch
			}
			if iSVariable(header.Name) {
				match.Conditions = append(match.Conditions, Condition{Variable: getVariable(header.Name),
					Value: value})
			} else {
				match.Conditions = append(match.Conditions, Condition{Header: header.Name,
					Value: value})
			}

		case xdsresource.SafeRegexMatcherType:
			regex := header.RegexMatch.String()
			if *invert {
				value = "!" + regex
			} else {
				value = regex
			}
			if iSVariable(header.Name) {
				match.Conditions = append(match.Conditions, Condition{Variable: getVariable(header.Name),
					Value: value})
			} else {
				match.Conditions = append(match.Conditions, Condition{Header: header.Name,
					Value: value})
			}
		case xdsresource.PresentMatcherType:
			if *header.PresentMatch {
				if iSVariable(header.Name) {
					match.Conditions = append(match.Conditions, Condition{Variable: getVariable(header.Name),
						Value: "*"})
				} else {
					match.Conditions = append(match.Conditions, Condition{Header: header.Name,
						Value: "*"})
				}
			}
		case xdsresource.PrefixMatcherType:
			if *invert {
				value = "!" + *header.PrefixMatch
			} else {
				value = *header.PrefixMatch
			}
			if iSVariable(header.Name) {
				match.Conditions = append(match.Conditions, Condition{Variable: getVariable(header.Name),
					Value: value})
			} else {
				match.Conditions = append(match.Conditions, Condition{Header: header.Name, Value: value})
			}
		}

	}

	for _, queryParameter := range route.QueryParams {
		switch queryParameter.MatchPattern {
		case xdsresource.ExactMatcherType:
			match.Conditions = append(match.Conditions, Condition{Argument: queryParameter.Name, Value: *queryParameter.ExactMatch})
		case xdsresource.SafeRegexMatcherType:
			regex := queryParameter.RegexMatch.String()
			match.Conditions = append(match.Conditions, Condition{Argument: queryParameter.Name, Value: "~" + regex})
		}
	}

	//Initialize the match action and splits
	if len(route.WeightedClusters) > 1 {
		match.Splits = make([]Split, 0, len(route.WeightedClusters))
		for upstreamName, wc := range route.WeightedClusters {
			match.Splits = append(match.Splits, Split{
				Weight: wc.Weight,
				Action: &Action{
					Pass: upstreamName,
				},
			})
		}
	} else {
		for upstreamName, _ := range route.WeightedClusters {
			match.Action.Pass = upstreamName
		}
	}

	return match
}

//Istio does not merge routes of the same path, so njet cannot process them
func MergeRouteForSamePath(routeName string, virtualHost *xdsresource.VirtualHost) map[string]*Route {
	routes := make(map[string]*Route)

	for _, route := range virtualHost.Routes {
		// init loction HTTPMatch
		var path string
		var pathType PathType
		switch route.UriMatchPattern {
		case xdsresource.ExactMatcherType:
			pathType = PathTypeExact
			path = "= " + *route.Path

		case xdsresource.PrefixMatcherType:
			pathType = PathTypePrefix
			path = "^~ " + *route.Prefix

		case xdsresource.SafeRegexMatcherType:
			pathType = PathTypeSafeRegex
			path = "~ " + route.Regex.String()
		}
		if _, ok := routes[path]; !ok {
			r := &Route{
				Path:     path,
				PathType: &pathType,
				Name:     route.Name,
				Matches:  []Match{},
				Action:   &Action{Pass: ""},
			}

			routes[path] = r
			//default action or splits
			if len(route.Headers) == 0 && len(route.QueryParams) == 0 {
				if len(route.WeightedClusters) > 1 {
					r.Splits = make([]Split, 0, len(route.WeightedClusters))
					for upstreamName, wc := range route.WeightedClusters {
						r.Splits = append(r.Splits, Split{
							Weight: wc.Weight,
							Action: &Action{
								Pass: upstreamName,
							},
						})
					}
				} else {
					for upstreamName, _ := range route.WeightedClusters {
						r.Action.Pass = upstreamName
					}
				}
			} else { //enable advanced content-based routing
				match := GenerateMatch(route)
				r.Matches = append(r.Matches, match)
			}
		} else { //Merge route for same path
			//default action or splits, but the same path has multiple destinations(It could be the same or it could be different)
			if len(route.Headers) == 0 && len(route.QueryParams) == 0 {
				if routes[path].Action.Pass != "" || len(routes[path].Splits) != 0 {
					//Use what you first found
				} else { //default action or splits  is later
					if len(route.WeightedClusters) > 1 {
						routes[path].Splits = make([]Split, 0, len(route.WeightedClusters))
						for upstreamName, wc := range route.WeightedClusters {
							routes[path].Splits = append(routes[path].Splits, Split{
								Weight: wc.Weight,
								Action: &Action{
									Pass: upstreamName,
								},
							})
						}
					} else {
						for upstreamName, _ := range route.WeightedClusters {
							routes[path].Action.Pass = upstreamName
						}
					}
				}

			} else { //enable advanced content-based routing
				match := GenerateMatch(route)
				routes[path].Matches = append(routes[path].Matches, match)
			}
		}
	}

	return routes
}

func GenerateVirtualServerConfig(routeName string, virtualHost *xdsresource.VirtualHost, mergeRoutes map[string]*Route) *VirtualServerConfig {
	server := &Server{
		Name:                      virtualHost.Name,
		RouteName:                 routeName,
		Locations:                 []*Location{},
		InternalRedirectLocations: []InternalRedirectLocation{},
	}
	server.Aliases = virtualHost.Domains
	for _, domain := range virtualHost.Domains {
		if domain != "*" {
			server.Hostnames = append(server.Hostnames, domain)
		}
	}

	var locations []*Location
	var internalRedirectLocations []InternalRedirectLocation
	var splitClients []SplitClient
	var maps []Map
	matchesRoutes := 0

	variableNamer := newVariableNamer(virtualHost)

	for _, r := range mergeRoutes {
		if len(r.Matches) > 0 {
			cfg := generateMatchesConfig(
				r,
				variableNamer,
				matchesRoutes,
				len(splitClients),
			)
			maps = append(maps, cfg.Maps...)
			locations = append(locations, cfg.Locations...)
			internalRedirectLocations = append(internalRedirectLocations, cfg.InternalRedirectLocation)
			splitClients = append(splitClients, cfg.SplitClients...)
			matchesRoutes++
		} else if len(r.Splits) > 1 {
			cfg := generateDefaultSplitsConfig(r, variableNamer, len(splitClients))
			splitClients = append(splitClients, cfg.SplitClients...)
			locations = append(locations, cfg.Locations...)
			internalRedirectLocations = append(internalRedirectLocations, cfg.InternalRedirectLocation)
		} else {
			upstreamName := r.Action.Pass
			loc := generateLocation(r.Path, upstreamName, false)
			locations = append(locations, &loc)
		}
	}

	server.InternalRedirectLocations = internalRedirectLocations
	server.Locations = locations
	vsCfg := &VirtualServerConfig{
		SplitClients: splitClients,
		Maps:         maps,
		HTTPSnippets: []string{},
		Server:       server,
		SpiffeCerts:  false,
		Name:         virtualHost.Name,
		MergeRoutes:  mergeRoutes,
	}

	return vsCfg
}
